package com.ym.core.config;

import cn.hutool.extra.spring.SpringUtil;
import com.ym.common.constant.UserDetails;
import com.ym.common.enums.UserUserTypeEnum;
import com.ym.core.annotation.PermissionAop;
import com.ym.core.domain.APIResponse;
import com.ym.core.redis.RedisKeyUtil;
import com.ym.core.shiro.ShiroUtil;
import com.ym.core.shiro.service.SysLoginService;
import com.ym.core.utils.PermissionUtils;
import com.ym.medical.domain.entity.SysUserEntity;
import com.ym.medical.domain.vo.SysRoleDataRuleListPcVO;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SubSelect;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

@Intercepts({
//        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
}
)
public class DataPermissionHelper implements Interceptor {
    private static final Logger log = LoggerFactory.getLogger(DataPermissionHelper.class);

    private StringRedisTemplate redisTemplate = SpringUtil.getBean(StringRedisTemplate.class);

    private SysLoginService mbsConnectSV = SpringUtil.getBean(SysLoginService.class);

    private Map<String, Integer> existJoin = new HashMap<>();

    // 这是对应上面的args的序号
    private static int MAPPED_STATEMENT_INDEX = 0;
    private static int PARAMETER_INDEX = 1;
    private static int ROWBOUNDS_INDEX = 2;
    private static int RESULT_HANDLER_INDEX = 3;
    private static int SQL_JOIN_NUM = 0;
    private static String SQL_JOIN_KEY = "0";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // todo 1. 校验登录类型
        if (invalidLoginType()) {
            return invocation.proceed();
        }

        // todo 2. 拼接用户机构编码
        SysUserEntity sysUser = (SysUserEntity) ShiroUtil.getLoginUser(true);
        sysUser.setAgency(appendAgency(sysUser.getAgency()));
        if (StringUtils.isBlank(sysUser.getAgency())) {
            return invocation.proceed();
        }

        // todo 3. 获取用户角色
        Collection<String> roles = redisTemplate.opsForSet().members(RedisKeyUtil.KEY_PREFIX + ":_ROLES_" + sysUser.getId());
        if (invalidRoles(roles)) {
            return invocation.proceed();
        }

        //todo 3.获取数据权限规则
        List<SysRoleDataRuleListPcVO> rules = SpringRunAfterConfig.sysRoleDataRules;
        if (invalidRules(rules)) {
            return invocation.proceed();
        }


        final Object[] queryArgs = invocation.getArgs();
        final MappedStatement mappedStatement = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX];
        final Object parameter = queryArgs[PARAMETER_INDEX];
        final BoundSql boundSql = mappedStatement.getBoundSql(parameter);


        //todo 4. 根据 StatementHandler 获取 注解对象
        if (invalidPermissionAop(mappedStatement)) {
            return invocation.proceed();
        }

        //todo 5. 开始拼接sql
        log.info("数据权限处理【拼接SQL】...");
        Map<String, String> params = new HashMap<>();
        String sql = processSelectSql(replaceGBKOrder(boundSql.getSql(), params), rules, roles, sysUser);
        if (StringUtils.isBlank(sql)) {
            log.info("没有可拼接的sql,直接放行...");
            return invocation.proceed();
        }
        //todo 6. 重新new一个查询语句对像
        BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), restoreGBKOrder(sql, params), boundSql.getParameterMappings(), boundSql.getParameterObject());

        //todo 6. 把新的查询放到statement里
        MappedStatement newMs = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
        for (ParameterMapping mapping : boundSql.getParameterMappings()) {
            String prop = mapping.getProperty();
            if (boundSql.hasAdditionalParameter(prop)) {
                newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
            }
        }
        queryArgs[MAPPED_STATEMENT_INDEX] = newMs;
        return invocation.proceed();
    }

    /**
     * 无效的注解 直接放行
     *
     * @param mappedStatement
     * @return
     */
    private boolean invalidPermissionAop(MappedStatement mappedStatement) {
        PermissionAop permissionAop = PermissionUtils.getPermissionByDelegate(mappedStatement);
        if (permissionAop == null) {
            log.info("数据权限放行...");
            return true;
        }
        return false;
    }

    /**
     * 无效的数据过滤规则 直接放行
     *
     * @param rules
     * @return
     */
    private boolean invalidRules(List<SysRoleDataRuleListPcVO> rules) {
        if (rules == null || rules.isEmpty()) {
            log.info("获取数据权限规则失败,直接放行...");
            return true;
        }
        return false;
    }


    /**
     * 无效的角色 直接放行
     *
     * @param roles
     * @return
     */
    private boolean invalidRoles(Collection<String> roles) {
        if (roles == null || roles.isEmpty()) {
            log.info("获取用户角色失败,直接放行...");
            return true;
        }
        return false;
    }

    /**
     * 拼接用户机构编码
     *
     * @param agency
     * @return
     */
    private String appendAgency(String agency) {
        if (StringUtils.isBlank(agency)) {
            log.debug("当前用户没有机构编码,直接放行...");
            return agency;
        }
        //获取子机构编码
        try {
            APIResponse response = mbsConnectSV.getOrgCodes(agency);
            List<Map<String, Object>> list = (List<Map<String, Object>>) response.getData();
            log.info("机构返回信息{}", list.toString());
            StringBuffer sbf = new StringBuffer();
            for (Map<String, Object> map : list) {
                sbf.append("'" + map.get("code") + "'").append(",");
            }
            agency = sbf.substring(0, sbf.length() - 1);
            log.info("机构拼接信息{}", agency);
        } catch (Exception e) {
            log.debug("获取机构编码异常！ex:{}", e.getMessage());
        }
        return agency;
    }

    /**
     * 存在无效的登录类型直接放行
     * 1.未登录放行
     * 2.获取用户类型失败放行
     * 3.会员用户直接放行
     *
     * @return
     */
    private boolean invalidLoginType() {
        Subject subject = ShiroUtil.getSubject();
        if (subject == null || subject.getPrincipals() == null) {
            log.info("未登录,直接放行...");
            return true;
        }

        String loginType = null;
        try {
            UserDetails loginUser = ShiroUtil.getLoginUser(true);
            if (loginUser != null) {
                loginType = loginUser.getUserTypeEnum().getKey();
            }

        } catch (Exception e) {
            e.getMessage();
        }
        if (StringUtils.isBlank(loginType)) {
            log.info("获取用户类型失败,直接放行...");
            return true;
        }

        if (!UserUserTypeEnum.SYSTEM.getKey().equals(loginType)) {
            log.info("会员用户直接放行...");
            return true;
        }
        return false;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 拼接sql逻辑
     *
     * @param sql
     * @param rules
     * @param roles
     * @param sysUser
     * @return
     */
    private String processSelectSql(String sql, List<SysRoleDataRuleListPcVO> rules, Collection<String> roles, SysUserEntity sysUser) {
        String agency = sysUser.getAgency();
        try {
            String replaceSql = null;
            Statement statement = CCJSqlParserUtil.parse(sql);
            Select select;
            if (statement instanceof Select) {
                select = (Select) CCJSqlParserUtil.parse(sql);
            } else {
                select = ((Update) CCJSqlParserUtil.parse(sql)).getSelect();
            }
            PlainSelect selectBody = (PlainSelect) select.getSelectBody();
            String mainTable = null;
            if (selectBody.getFromItem() instanceof Table) {
                mainTable = ((Table) selectBody.getFromItem()).getName().replace("`", "");
            } else if (selectBody.getFromItem() instanceof SubSelect) {
                replaceSql = processSelectSql(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), rules, roles, sysUser);
            }
            if (!StringUtils.isEmpty(replaceSql)) {
                // sql = replaceSql;
                sql = selectBody.toString().replace(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), replaceSql);
            }
            String mainTableAlias = mainTable;


            String condExpr;
            SysRoleDataRuleListPcVO realRuls = null;
            for (SysRoleDataRuleListPcVO rule : rules) {
                String[] roleTmps = rule.getRoles().split(",");
                for (String roleStr : roleTmps) {
                    if (roles.toString().contains(roleStr)) {
                        if (rule.getFromEntity().contains("," + mainTable + ",")) {

                            // 若主表匹配规则主体，则直接使用本规则
                            realRuls = rule;

                            try {
                                mainTableAlias = selectBody.getFromItem().getAlias().getName();
                            } catch (Exception e) {
                                log.debug("当前sql中， " + mainTable + " 没有设置别名");
                            }
                            mainTableAlias = StringUtils.isNotBlank(mainTableAlias) ? mainTableAlias + "." : "";

                            condExpr = mainTableAlias + rule.getExps()
                                    .replace("{uid}", sysUser.getId() + "")
                                    .replace("{org_code}", agency);
                            if (StringUtils.isNotBlank(mainTable)) {
                                condExpr = condExpr.replace("{me}", mainTable);
                            }
                            if (StringUtils.isNotBlank(mainTableAlias)) {
                                condExpr = condExpr.replace("{me.a}", mainTableAlias);
                            }
                            if (selectBody.getWhere() == null) {
                                selectBody.setWhere(CCJSqlParserUtil.parseCondExpression(condExpr));
                            } else {
                                AndExpression and = new AndExpression(selectBody.getWhere(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                selectBody.setWhere(and);
                            }
                        }


                        //--------------------------------------------处理sql关联--------------------------------------------------------------
                        try {
                            String joinTable = null;
                            String joinTableAlias = null;
                            List<Join> joins = selectBody.getJoins();
                            if (joins != null && !joins.isEmpty()) {
                                for (Join j : joins) {
                                    try {
                                        joinTable = ((Table) j.getRightItem()).getName();
                                    } catch (Exception e) {
                                        log.debug("当前sql不是真实的表名,{}", j.getRightItem());
                                        continue;   //不支持子查询 当前sql join不是直接关联表名
                                    }

                                    if (rule.getFromEntity().contains("," + joinTable + ",")) {
                                        // 当主表不能匹配时，匹配所有join，使用符合条件的第一个表的规则。
                                        try {
                                            joinTableAlias = j.getRightItem().getAlias().getName();
                                        } catch (Exception e) {
                                            log.debug("获取join表别名失败,{}", j.getRightItem());
                                        }
                                        realRuls = rule;
                                        joinTableAlias = StringUtils.isNotBlank(joinTableAlias) ? joinTableAlias + "." : "";

                                        condExpr = joinTableAlias + rule.getExps()
                                                .replace("{uid}", sysUser.getId() + "")
                                                .replace("{org_code}", agency);
                                        if (StringUtils.isNotBlank(joinTable)) {
                                            condExpr = condExpr.replace("{me}", joinTable);
                                        }
                                        if (StringUtils.isNotBlank(joinTableAlias)) {
                                            condExpr = condExpr.replace("{me.a}", joinTableAlias);
                                        }
                                        if (j.getOnExpression() == null) {
                                            j.setOnExpression(CCJSqlParserUtil.parseCondExpression(condExpr));
                                        } else {
                                            AndExpression and = new AndExpression(j.getOnExpression(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                            j.setOnExpression(and);
                                        }
                                    }
                                }
                            }
                        } catch (Exception e) {
                            log.debug("当前sql没有join的部分！");
                        }
                    }
                }
            }
            if (realRuls == null) {
                return sql;
            }// 没有合适规则直接退出。

            if (sql.contains("limit ?,?") && select.toString().contains("LIMIT ? OFFSET ?")) {
                sql = select.toString().replace("LIMIT ? OFFSET ?", "limit ?,?");
            } else {
                sql = select.toString();
            }

        } catch (JSQLParserException e) {
            log.error("change sql error .", e);
        }
        return sql;
    }

    private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            builder.keyProperty(ms.getKeyProperties()[0]);
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    public static class BoundSqlSqlSource implements SqlSource {
        private BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }

    private String replaceGBKOrder(String sql, Map<String, String> params) {
        int start = sql.indexOf("CONVERT(");
        if (start != -1) {
            int end = sql.indexOf("USING GBK)") + 10;
            String replaceSql = sql.substring(start, end);
            String param = "order_sql" + (params.size() + 1);
            params.put(param, replaceSql);
            return replaceGBKOrder(sql.replace(replaceSql, " " + param + " "), params);
        } else {
            return sql;
        }
    }

    private String restoreGBKOrder(String sql, Map<String, String> params) {
        if (!params.isEmpty()) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                sql = sql.replace(entry.getKey(), entry.getValue());
            }
        }
        return sql;
    }

}
